#!/usr/bin/env python
##
#    Exchange
#         
#    Exchange interaction class (via IMAP/SMTP)
#    
#    @author     David Lasley, dave -at- dlasley -dot- net
#    @package    pyph-imap
#    @version    $Id: exchange.py 2 2012-12-12 19:35:18Z dave@dlasley.net $
import ConfigParser, imaplib, os, re, smtplib, string, sys
from email.MIMEMultipart import MIMEMultipart
from email.MIMEImage import MIMEImage
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Utils import formatdate
from email import Encoders, message_from_string, parser
from socket import gethostname
from wins.toolbox.pass_extract import pass_extract #< http://code.google.com/p/pass-extract/

class exchange(object):
    '''
        Main exchange interaction class
    '''
    cleanup_files = []
    __component__ = 'mail_check'
    _expunge_bool = False
    def __init__(self,verbose=False):
        self.verbose    =   verbose
        adCreds = pass_extract('TeamReporting','AD') #< http://code.google.com/p/pass-extract/
        self.hostname   =   'exchange.COMPANY.com'
        self.port       =   '465'
        self.username   =   adCreds.username
        self.password   =   adCreds.current_pass
        self.conn       =   self.login()
        self.current_mailbox    =   False
        self.email_ini =   '/var/www/html/shared_data/emailData.ini'
        self.temp_dir = '/data01/temp'
        self.debug = False if gethostname() == 'cwch3wnsapvl001.na.corp.COMPANY.com' else True
    
    def __del__(self):
        '''
            On exit
        '''
        try:
            self.expunge_conn()
        except: pass
        try:
            if self.current_mailbox != False:
                if self.verbose: print 'Closing %s' % self.current_mailbox['name']
                self.conn.close()
        except: pass
        try:
            if self.conn is not None:
                if self.verbose: print 'Logging out of %s' % self.hostname
                self.conn.logout()
        except: pass
        self.del_temp_files()
    
    def file_put_contents(self,filepath,string,mode='w'):
        '''
            Method to put contents of string into file
            
            @param  String  filepath    File path to make
            @param  String  string      String to write to file
            @param  String  mode        Write mode, defaults to w
            
            @return Bool    Success/Fail
        '''
        try:
            fh = open(filepath,mode)
            fh.write(string)
            fh.close()
            return True
        except:
            raise Exception('Cannot write file',
                                    component=self.__component__,
                                    message='%s\r%s' % (filepath,sys.exc_info()))

    def login(self):
        '''
            Login to exchange server
            
            @return imaplib.IMAP4_SSLObject
        '''
        try:
            if self.verbose: print 'Connecting to %s' % self.hostname
            conn = imaplib.IMAP4_SSL(self.hostname)
            
            if self.verbose: print 'Logging in as %s' % self.username
            conn.login(self.username, self.password)
        except:
            raise Exception('Cannot login',
                                    component=self.__component__,
                                    message='%s' % repr(sys.exc_info()))
        return conn
    
    def get_mailboxes(self,list_pattern=False):
        '''
            List mailboxes
            
            @param  String  list_pattern    Look for certain mailbox name pattern?
            
            @return List    List of mailboxes
        '''
        try:
            mailboxes = {}
            list_response_pattern = re.compile(r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
            if list_pattern:
                list_pattern = "*%s*" % list_pattern
                typ, raw_mailboxes = self.conn.list(pattern=list_pattern)
            else:
                typ, raw_mailboxes = self.conn.list()
            for line in raw_mailboxes:
                flags, delimiter, mailbox_name = list_response_pattern.match(line).groups()
                mailbox_name = mailbox_name.strip('"')
                mailboxes[mailbox_name] = {'flags':flags, 'delimiter':delimiter}
            if self.verbose:
                print 'Response code: %s' % typ
                print 'Server Response:'
                print raw_mailboxes
                print 'Processed Response:'
                print mailboxes
        except:
            raise Exception('Cannot get mailboxes',
                                    component=self.__component__,
                                    message='%s\r%s' % (list_pattern,repr(sys.exc_info())))
        return mailboxes
    
    def switch_mailbox(self,new_mailbox):
        '''
            Switch active mailbox
            
            @param  String  new_mailbox New Mailbox Name
            
            @return Bool    Success/Fail
        '''
        try:
            typ, data = self.conn.select(new_mailbox)
            if typ != "NO":
                num_msgs = int(data[0])
                self.current_mailbox = {'name':new_mailbox, 'msgs':num_msgs}
                return_val = True
                if self.verbose:
                    print typ, data
                    print "There are %d messages in %s" % (num_msgs, new_mailbox)
            else:
                return_val = False
                if self.verbose:
                    print typ, data
        except:
            raise Exception('Cannot switch mailbox',
                                    component=self.__component__,
                                    message='%s\r%s' % (new_mailbox,repr(sys.exc_info())))
        return return_val
    
    def move_message(self,message_id,move_to,delete=True):
        '''
            Move message_id to move_to
            
            @param  Int     message_id  Message ID to manipulate
            @param  Str     move_to     Folder to move to
            @return Bool    Success/Fail (after also deleting message if delete)
        '''
        result = self.conn.copy(message_id, move_to)
        if result[0] == 'OK':
            return self.delete_message(message_id) if delete else True
        else:
            raise Exception('Message move issue',
                                component=self.__component__,
                                message='ID: %s\rMoving To:\r%s' % (message_id,move_to,result))
        
    def delete_message(self,message_id):
        '''
            Delete message, flag conn for expunge
            
            @param  Int     message_id  Message ID to manipulate
            @return Bool    Success/fail
        '''
        typ, result = self.conn.store(message_id, '+FLAGS', r'(\Deleted)')
        #if result[0] == 'OK':
        self._expunge_bool = True
        return True
        #else:
        #    raise Exception('Message delete issue',
        #                        component=self.__component__,
        #                        message='ID: %s\r%s' % (message_id,result))
    
    def expunge_conn(self):
        '''
            Expunge connection if self._expunge_bool
            
            @return Bool    Success/Fail
        '''
        if self._expunge_bool:
            self.conn.expunge()
            self._expunge_bool = False
        return True
    
    def message_search(self,search='ALL'):
        '''
            Search for a message string in mailbox
            
            @param  String  search  message search string as defined at imaplib.IMAP4.search()
            
            @return List    List of message IDs matching search
        '''
        try:
            searchStr = '('
            addStr = ''
            if type(search) is dict:
                for key in search:
                   searchStr +=  '%s%s "%s"' % (addStr, key, search[key])
                   addStr = ' ' 
                searchStr += ")"
            else:
                searchStr = search
            typ, msg_ids = self.conn.search(None, searchStr)
            if self.verbose:
                print "Searching %s" % searchStr
                print "Response: %s" % typ
                print "Message IDS: %s" % msg_ids
            return msg_ids[0].split(' ')
        except:
            raise Exception('Message search fail',
                                    component=self.__component__,
                                    message='%s\r%s' % (search,repr(sys.exc_info())))
    
    def fetch(self, msg_id, parts, peek=False, header_fields=False):
        '''
            Fetch message
            
            @param  Int     msg_id          Message ID to fetch
            @param  List    parts           Parts of message to retrieve (header,body,flags,whole)
            @param  Bool    peek            Only return message headers
            @param  List   header_fields    Header fields to return
            
            @return Tuple   Fetched message
        '''
        try:
            int(msg_id)
            try:
                fetchStrs   =   {
                                    True    :   "BODY.PEEK",
                                    False   :   "BODY",
                                }
                if header_fields:
                    header_fields = '.FIELDS (%s)' % ' '.join(header_fields);
                partsStrs   =   {
                                    "header"    :   "%s[HEADER%s]"  %   (fetchStrs[peek],header_fields),
                                    "body"      :   "%s[TEXT]"      %   fetchStrs[peek],
                                    "flags"     :   "FLAGS",
                                    'whole'     :   "RFC822"
                                }
                responses = msg_datas = {}
                for i in parts:
                    responses[i], msg_datas[i] = self.conn.fetch(msg_id, "(%s)" % partsStrs[i])
                return msg_datas
            except:
                raise Exception('Cannot fetch message',
                                        component=self.__component__,
                                        message='Message ID: "%s"\r%s' % (msg_id,repr(sys.exc_info())))
        except ValueError:
            return False
    
    def send_message(self,send_to,bodyHTML,subject='Report Delivery',
                    cc_to = ['david.lasley@COMPANY.com','theodore.salmon@COMPANY.com'],send_from='DATAIT <TeamReporting@COMPANY.com>',
                    attachments=None,bodyTxt=None,internal=False,bcc_to=None,originalMsg='', formatted=True):
        '''
            Send message
            
            @todo   Add inline attachments
            
            @param  List    send_to     Addresses to send to
            @param  String  bodyHTML    Message body data
            @param  String  subject     Subject of message
            @param  List    cc_to       Addresses to CC
            @param  String  send_from   Address to send from
            @param  List    attachments List of attachment file paths
            @param  String  bodyTxt     Text to display to non-html email clients
            @param  Bool    internal    Internal email (place internal signature)?
            @param  List    bcc_to      BCC
            @param  String  originalMsg Original Message string to add to bottom of message (replies)
            @param  Bool    formatted   Use preformatting as defined in ini?
        '''
        try:
            _msg            =   MIMEMultipart('related') if formatted else MIMEText(bodyHTML,'plain')
            if self.debug:
                send_to = ['david.lasley@COMPANY.com','theodore.salmon@COMPANY.com']
                cc_to = []
                bcc_to = []
                subject = '%s (DEV BOX)' % subject
            _msg['From']    =   send_from
            _msg['To']      =   ', '.join(send_to)
            _msg['CC']      =   ', '.join(cc_to)
            if bcc_to is not None:
                _msg['BCC']      =   ', '.join(bcc_to)
            else:
                bcc_to = []
            _msg['Date']    =   formatdate(localtime=True)
            _msg['Subject'] =   subject
            _msg.add_header('reply-to', 'Team Team <TeamTeam@COMPANY.com')
            if formatted:
                email_config    =   self.__load_config(self.email_ini)
                _msg.preamble   =   'This is a multi-part message in MIME format.'
                #   Text
                bodyTxt = bodyTxt if bodyTxt is not None else email_config['email.txtbody']
                _msgAlternative = MIMEMultipart('alternative')
                _msg.attach(_msgAlternative)
                _msgAlternative.attach(MIMEText(bodyTxt,'plain','UTF-8'))
                #   HTML
                emailInternal = email_config['email.internal'] if internal else ''
                footer =  email_config['email.mailbottom'].replace('$internal',emailInternal)
                bodyHTML = u'%s %s %s' %(email_config['email.mailtop'],bodyHTML.decode('utf8'),footer)
                _msgAlternative.attach(MIMEText(bodyHTML.replace("$originalMsg",originalMsg),'html','UTF-8'))
                #   Logo Attach
                fh = open(email_config['email.logopath'], 'rb')
                _logoImg = MIMEImage(fh.read())
                fh.close()
                _logoImg.add_header('Content-ID','<%s>' % email_config['email.logoid'])
                _msg.attach(_logoImg)
                #   Loop Attachments
                if attachments is not None:
                    for attachment in attachments:
                        part = MIMEBase('application', "octet-stream")
                        part.set_payload( open(attachment['path'],"rb").read() )
                        Encoders.encode_base64(part)
                        try:
                            part.add_header('Content-Disposition', 'attachment; filename="%s"' % attachment['filename'])
                        except:
                            part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(attachment['path']))
                        _msg.attach(part)
            else:
                bodyHTML = "%s" %(bodyHTML)
            smtp = smtplib.SMTP(self.hostname)
            smtp.starttls()
            smtp.login(self.username,self.password)
            smtp.set_debuglevel(self.verbose)
            smtp.sendmail(send_from, send_to+cc_to+bcc_to, _msg.as_string())
            smtp.quit()
        except:
            raise Exception('Cannot send message',
                                component=self.__component__,
                                message='%s' % repr(sys.exc_info()))
        
    def del_temp_files(self):
        '''
            Cleanup files
        '''
        for file_path in self.cleanup_files:
            try:
                os.unlink(file_path)
            except OSError:
                pass
        self.cleanup_files = []
    
    def extract_body(self,message):
        '''
            Extract body of email
            
            @param  email.message   message Message object to be parsed
            
            @return String  Message payload
        '''
        if message.get_content_maintype() != 'multipart':
            return message.get_payload()
        else:
            for part in message.walk():
                if part.get_content_maintype() != 'multipart':
                    return part.get_payload(decode=True)
    
    def get_attachments(self,email_body):
        '''
            Extract attachment from emails
            
            @param  String  email_body  Email body string
            
            @return Tuple   New attachment file paths
        '''
        try:
            mail = message_from_string(email_body)
            attachments = []
            if mail.get_content_maintype() != 'multipart':
                return mail.get_content_maintype()
            for part in mail.walk():
                if part.get_content_maintype() == 'multipart':
                    continue
                if part.get('Content-Disposition') is None:
                    continue
                filename = part.get_filename()
                att_path = os.path.join(self.temp_dir, filename)
                fp = open(att_path, 'wb')
                fp.write(part.get_payload(decode=True))
                fp.close()
                attachments.append(att_path)
                self.cleanup_files.append(att_path)
            return attachments
        except:
            raise Exception('Cannot save attachments',
                             component=self.__component__,
                             message='%s\r%s' % (att_path,repr(sys.exc_info())))
    
    def __load_config(self, filein, config={}):
        '''
            Load email ini configuration
            
            @param  Dict    config  Don't touch this!
            
            @return Dict    Config
        '''
        config = config.copy()
        cp = ConfigParser.ConfigParser()
        try:
            cp.read(filein)
        except:
            raise Exception('Problem parsing config',
                             component=self.__component__,
                             message='%s\r%s' % (filein,repr(sys.exc_info())))
        for sec in cp.sections():
            name = string.lower(sec)
            for opt in cp.options(sec):
                config[name + "." + string.lower(opt)] = string.strip(cp.get(sec, opt))[1:-1].replace('\\"','"')
        return config